/*
 * Example Of using Token inheritance to support dynamically defined Tokens.
 * In this example an 'array' language [1, 2, 3, 4, ...] where additional delimiters can be
 * defined by the user instead of being limited to just the 'built in' comma delimiter.
 *
 * This is made possible because the Lexer itself is created directly in javascript (no code generation).
 * And the Token matching during parsing uses the 'instanceof' operator.
 * This the Parser need not be modified to support each new custom delimiter.
 *
 * Note that it is mandatory to enable the "dynamicTokensEnabled" config property for this capability to work.
 * Otherwise certain performance optimizations may break as those assume that the Token vocabulary is static.
 */
import { createToken, Lexer, EmbeddedActionsParser } from "chevrotain";

// ----------------- lexer -----------------
const LSquare = createToken({ name: "LSquare", pattern: /\[/ });
const RSquare = createToken({ name: "RSquare", pattern: /]/ });

// base delimiter TokenTypes
const BaseDelimiter = createToken({ name: "BaseDelimiter", pattern: Lexer.NA });
const Comma = createToken({
  name: "Comma",
  pattern: /,/,
  categories: BaseDelimiter,
});
const NumberLiteral = createToken({ name: "NumberLiteral", pattern: /\d+/ });
const WhiteSpace = createToken({
  name: "WhiteSpace",
  pattern: /\s+/,
  group: Lexer.SKIPPED,
});

const allTokens = [
  WhiteSpace,
  LSquare,
  RSquare,
  BaseDelimiter,
  Comma,
  NumberLiteral,
];

// ----------------- parser -----------------
class DynamicDelimiterParser extends EmbeddedActionsParser {
  constructor() {
    super(allTokens, {
      // by default the error recovery / fault tolerance capabilities are disabled
      // use this flag to enable them
      recoveryEnabled: true,
      // IMPORTANT: must be enabled to support dynamically defined Tokens
      dynamicTokensEnabled: true,
    });

    // not mandatory, using <$> (or any other sign) to reduce verbosity (this. this. this. this. .......)
    const $ = this;

    this.RULE("array", () => {
      let result = "";

      $.CONSUME(LSquare); // This will match any Token Class which extends BaseLeftDelimiter
      $.OPTION(() => {
        result += $.CONSUME(NumberLiteral).image;
        $.MANY(() => {
          $.CONSUME(BaseDelimiter);
          result += $.CONSUME2(NumberLiteral).image;
        });
      });
      $.CONSUME(RSquare); // This will match any Token Class which extends BaseRightDelimiter

      return result;
    });

    // very important to call this after all the rules have been defined.
    // otherwise, the parser may not work correctly as it will lack information
    // derived during the self-analysis phase.
    this.performSelfAnalysis();
  }
}

// ----------------- wrapping it all together -----------------

// reuse the same parser instance.
const parser = new DynamicDelimiterParser();

export function parse(text, dynamicDelimiterRegExp) {
  // make this parameter optional
  if (dynamicDelimiterRegExp === undefined) {
    dynamicDelimiterRegExp = Lexer.NA;
  }

  // dynamically create Token classes which extend the BaseXXXDelimiters
  const dynamicDelimiter = createToken({
    name: "dynamicDelimiter",
    pattern: dynamicDelimiterRegExp,
    categories: BaseDelimiter,
  });

  // dynamically create a Lexer which can Lex all our language including the dynamic delimiters.
  const dynamicDelimiterLexer = new Lexer(allTokens.concat([dynamicDelimiter]));

  // lex
  const lexResult = dynamicDelimiterLexer.tokenize(text);

  // setting the input will reset the parser's state
  parser.input = lexResult.tokens;
  // parse
  const value = parser.array();

  return {
    value: value,
    lexErrors: lexResult.errors,
    parseErrors: parser.errors,
  };
}
